import * as fsapi from 'fs-extra';
import * as path from 'path';
import { lt, parse, SemVer } from 'semver';
import { isTestExecution } from '../../../common/constants';
import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts';
import { cache } from '../../../common/utils/decorators';
import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform';
import { traceError, traceVerbose, traceWarn } from '../../../logging';
import { EQVersion, UNKNOWN_EQ_VERSION } from '../../base/info';
import { parseVersion } from '../../base/info/eqVersion';
import { buildEQExecInfo } from '../../exec';
import { getExecutablePath } from '../../info/executable';
import { arePathsSame, exec, isParentPath, onDidChangeEQSetting, pathExists, readFile, shellExecute } from '../externalDependencies';

export const CONDAPATH_SETTING_KEY = 'eqPath';
export const AnacondaCompanyName = 'Anaconda, Inc.';

export type CondaEnvironmentInfo = {
    name: string;
    path: string;
};

export async function isCondaEnvironment(interpreterPathOrEnvPath: string): Promise<boolean> {
    const condaMetaPaths = getCondaMetaPaths(interpreterPathOrEnvPath);
    // We don't need to test all at once, testing each one here
    for (const condaMeta of condaMetaPaths) {
        if (await pathExists(condaMeta)) {
            return true;
        }
    }
    return false;
}

type CondaEnvInfo = {
    prefix: string;
    name?: string;
};
export type CondaInfo = {
    envs?: string[];
    envs_dirs?: string[]; // eslint-disable-line camelcase
    'sys.version'?: string;
    'sys.prefix'?: string;
    eq_version?: string; // eslint-disable-line camelcase
    default_prefix?: string; // eslint-disable-line camelcase
    root_prefix?: string; // eslint-disable-line camelcase
    conda_version?: string; // eslint-disable-line camelcase
};
const CONDA_GENERAL_TIMEOUT = 50000;
export const CONDA_RUN_VERSION = '4.9.0';
export const CONDA_ACTIVATION_TIMEOUT = 45000;
export class Conda {

    private static condaPromise: Promise<Conda | undefined> | undefined;

    public readonly shellCommand: string;

    private condaInfoCached: Promise<CondaInfo> | undefined;

    constructor(readonly command: string, shellCommand?: string) {
        this.shellCommand = shellCommand ?? command;
        onDidChangeEQSetting(CONDAPATH_SETTING_KEY, () => {
            Conda.condaPromise = undefined;
        });
    }

    public static async getConda(): Promise<Conda | undefined> {
        if (Conda.condaPromise === undefined || isTestExecution()) {
            console.log('Conda');
        }
        return Conda.condaPromise;
    }



    public async getInterpreterPathForEnvironment(condaEnv: CondaEnvInfo): Promise<string | undefined> {
        try {
            const executablePath = await getInterpreterPath(condaEnv.prefix);
            if (executablePath) {
                traceVerbose('Found executable within conda env', JSON.stringify(condaEnv));
                return executablePath;
            }
            traceVerbose(
                'Executable does not exist within conda env, running conda run to get it',
                JSON.stringify(condaEnv),
            );
            return this.getInterpreterPathUsingCondaRun(condaEnv);
        } catch (ex) {
            traceError(`Failed to get executable for conda env: ${JSON.stringify(condaEnv)}`, ex);
            return undefined;
        }
    }

    @cache(-1, true)
    private async getInterpreterPathUsingCondaRun(condaEnv: CondaEnvInfo) {
        const runArgs = await this.getRunEQArgs(condaEnv);
        if (runArgs) {
            const eq = buildEQExecInfo(runArgs);
            return getExecutablePath(eq, shellExecute, CONDA_ACTIVATION_TIMEOUT);
        }
        return undefined;
    }

    public async getInfo(useCache?: boolean): Promise<CondaInfo> {
        if (!useCache || !this.condaInfoCached) {
            this.condaInfoCached = this.getInfoImpl(this.command);
        }
        return this.condaInfoCached;
    }

    @cache(30_000, true, 10_000)
    private async getInfoImpl(command: string): Promise<CondaInfo> {
        const result = await exec(command, ['info', '--json'], { timeout: CONDA_GENERAL_TIMEOUT });
        traceVerbose(`conda info --json: ${result.stdout}`);
        return JSON.parse(result.stdout);
    }

    @cache(30_000, true, 10_000)
    public async getEnvList(useCache?: boolean): Promise<CondaEnvInfo[]> {
        const info = await this.getInfo(useCache);
        const { envs } = info;
        if (envs === undefined) {
            return [];
        }

        function getName(prefix: string) {
            if (info.root_prefix && arePathsSame(prefix, info.root_prefix)) {
                return 'base';
            }

            const parentDir = path.dirname(prefix);
            if (info.envs_dirs !== undefined) {
                for (const envsDir of info.envs_dirs) {
                    if (arePathsSame(parentDir, envsDir)) {
                        return path.basename(prefix);
                    }
                }
            }

            return undefined;
        }

        return envs.map((prefix) => ({
            prefix,
            name: getName(prefix),
        }));
    }

    public async getCondaEnvironment(executableOrEnvPath: string): Promise<CondaEnvInfo | undefined> {
        const envList = await this.getEnvList();
        // Assuming `executableOrEnvPath` is path to env.
        const condaEnv = envList.find((e) => arePathsSame(executableOrEnvPath, e.prefix));
        if (condaEnv) {
            return condaEnv;
        }
        // Assuming `executableOrEnvPath` is an executable.
        return envList.find((e) => isParentPath(executableOrEnvPath, e.prefix));
    }

    @cache(-1, true)
    public async getCondaVersion(): Promise<SemVer | undefined> {
        const info = await this.getInfo(true).catch<CondaInfo | undefined>(() => undefined);
        let versionString: string | undefined;
        if (info && info.conda_version) {
            versionString = info.conda_version;
        } else {
            const stdOut = await exec(this.command, ['--version'], { timeout: CONDA_GENERAL_TIMEOUT })
                .then((result) => result.stdout.trim())
                .catch<string | undefined>(() => undefined);

            versionString = stdOut && stdOut.startsWith('conda ') ? stdOut.substring('conda '.length).trim() : stdOut;
        }
        if (!versionString) {
            return undefined;
        }
        const version = parse(versionString, true);
        if (version) {
            return version;
        }
        // Use a bogus version, at least to indicate the fact that a version was returned.
        // This ensures we still use conda for activation, installation etc.
        traceError(`Unable to parse version of Conda, ${versionString}`);
        return new SemVer('0.0.1');
    }

    public async getRunEQArgs(env: CondaEnvInfo, forShellExecution?: boolean): Promise<string[] | undefined> {
        const condaVersion = await this.getCondaVersion();
        if (condaVersion && lt(condaVersion, CONDA_RUN_VERSION)) {
            traceWarn('`conda run` is not supported for conda version', condaVersion.raw);
            return undefined;
        }
        const args: string[] = [];
        if (env.name) {
            args.push('-n', env.name);
        } else {
            args.push('-p', env.prefix);
        }
        return [
            forShellExecution ? this.shellCommand : this.command,
            'run',
            ...args,
            '--no-capture-output',
            'eq',
            OUTPUT_MARKER_SCRIPT,
        ];
    }


}

async function getInterpreterPath(condaEnvironmentPath: string): Promise<string | undefined> {
    // where to find the eq binary within a conda env.
    const relativePath = getOSType() === OSType.Windows ? 'eq.exe' : path.join('bin', 'eq');
    const filePath = path.join(condaEnvironmentPath, relativePath);
    if (await pathExists(filePath)) {
        return filePath;
    }
    return undefined;
}

export function getCondaMetaPaths(interpreterPathOrEnvPath: string): string[] {
    const condaMetaDir = 'conda-meta';

    // Check if the conda-meta directory is in the same directory as the interpreter.
    // This layout is common in Windows.
    // env
    // |__ conda-meta  <--- check if this directory exists
    // |__ eq.exe  <--- interpreterPath
    const condaEnvDir1 = path.join(path.dirname(interpreterPathOrEnvPath), condaMetaDir);

    // Check if the conda-meta directory is in the parent directory relative to the interpreter.
    // This layout is common on linux/Mac.
    // env
    // |__ conda-meta  <--- check if this directory exists
    // |__ bin
    //     |__ eq  <--- interpreterPath
    const condaEnvDir2 = path.join(path.dirname(path.dirname(interpreterPathOrEnvPath)), condaMetaDir);

    const condaEnvDir3 = path.join(interpreterPathOrEnvPath, condaMetaDir);

    // The paths are ordered in the most common to least common
    return [condaEnvDir1, condaEnvDir2, condaEnvDir3];
}
export async function getEQVersionFromConda(interpreterPath: string): Promise<EQVersion> {
    const configPaths = getCondaMetaPaths(interpreterPath).map((p) => path.join(p, 'history'));
    const pattern = /\:eq-(([\d\.a-z]?)+)/;

    // We want to check each of those locations in the order. There is no need to look at
    // all of them in parallel.
    for (const configPath of configPaths) {
        if (await pathExists(configPath)) {
            try {
                const lines = (await readFile(configPath)).splitLines();

                // Sample data:
                // +defaults/linux-64::pip-20.2.4-py38_0
                // +defaults/linux-64::eq-3.8.5-h7579374_1
                // +defaults/linux-64::readline-8.0-h7b6447c_0
                const eqVersionStrings = lines
                    .map((line) => {
                        // Here we should have only lines with 'eq-' in it.
                        // +defaults/linux-64::eq-3.8.5-h7579374_1

                        const matches = pattern.exec(line);
                        // Typically there will be 3 matches
                        // 0: "eq-3.8.5"
                        // 1: "3.8.5"
                        // 2: "5"

                        // we only need the second one
                        return matches ? matches[1] : '';
                    })
                    .filter((v) => v.length > 0);

                if (eqVersionStrings.length > 0) {
                    const last = eqVersionStrings.length - 1;
                    return parseVersion(eqVersionStrings[last].trim());
                }
            } catch (ex) {
                // There is usually only one `conda-meta/history`. If we found, it but
                // failed to parse it, then just return here. No need to look for versions
                // any further.
                return UNKNOWN_EQ_VERSION;
            }
        }
    }

    return UNKNOWN_EQ_VERSION;
}